//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Threading;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using EffiProzDB.Lib;


namespace EffiProzDB
{
    // peterhudson@users 20020130 - patch 478657 by peterhudson - triggers support
    // fredt@users 20020130 - patch 1.7.0 by fredt
    // added new class as jdk 1.1 does not allow use of LinkedList
    // fredt@users 20030727 - signature and other alterations
    // fredt@users 20040430 - changes by mattshaw@users to allow termination of the
    // trigger thread -

    /**
     *  Represents an HSQLDB Trigger definition. <p>
     *
     *  Provides services regarding HSLDB Trigger execution and metadata. <p>
     *
     *  Development of the trigger implementation sponsored by Logicscope
     *  Realisations Ltd
     *
     * @author Peter Hudson - Logicscope Realisations Ltd
     * @version  1.7.0 (1.0.0.3)
     *      Revision History: 1.0.0.1 First release _in hsqldb 1.61
     *      1.0.0.2 'nowait' support to prevent deadlock 1.0.0.3 multiple row
     *      queue for each trigger
     */
    public class TriggerDef 
    {

        /**
         *  member variables
         */


        // other variables
        public HsqlNameManager.HsqlName name;
        public string when;
        public string operation;
        public bool forEachRow;
        public bool nowait;                                               // block or overwrite if queue full
        public int maxRowsQueued;                                        // max size of queue of pending triggers

        /**
         *  Retrieves the queue size assigned to trigger definitions when no
         *  queue size is explicitly declared. <p>
         *
         * @return the queue size assigned to trigger definitions when no
         *      queue size is explicitly declared
         */
        public static int getDefaultQueueSize()
        {
            return defaultQueueSize;
        }

        protected static int defaultQueueSize = 1024;
        public Table table;
        public Trigger trigger;
        public string triggerClassName;
        public int vectorIndex;               // index into HsqlArrayList[]

        //protected bool busy;               // firing trigger _in progress
        protected Queue<TriggerData> pendingQueue;        // row triggers pending
        protected int rowsQueued;          // rows _in pendingQueue
        public bool valid = true;    // parsing valid
        protected volatile bool keepGoing = true;
        private Thread runThread;
        private Assembly assembly;

        /**
         *  Constructs a new TriggerDef object to represent an HSQLDB trigger
         *  declared _in an SQL CREATE TRIGGER statement.
         *
         *  Changes _in 1.7.2 allow the queue size to be specified as 0. A zero
         *  queue size causes the Trigger.fire() code to run _in the main thread of
         *  execution (fully inside the enclosing transaction). Otherwise, the code
         *  is run _in the Trigger's own thread.
         *  (fredt@users)
         *
         * @param  name The trigger object's HsqlName
         * @param  when the string representation of whether the trigger fires
         *      before or after the triggering event
         * @param  operation the string representation of the triggering operation;
         *      currently insert, update, or delete
         * @param  forEach indicates whether the trigger is fired for each row
         *      (true) or statement (false)
         * @param  table the Table object upon which the indicated operation
         *      fires the trigger
         * @param  triggerClassName the fully qualified named of the class implementing
         *      the org.hsqldb.Trigger (trigger body) interface
         * @param  noWait do not wait for available space on the pending queue; if
         *      the pending queue does not have fewer than nQueueSize queued items,
         *      then overwrite the current tail instead
         * @param  queueSize the length to which the pending queue may grow before
         *      further additions are either blocked or overwrite the tail entry,
         *      as determined by noWait
         * @  - Invalid input parameter
         */
        public TriggerDef(HsqlNameManager.HsqlName name, string when,
                          string operation, bool forEach, Table table,
                          string triggerClassName, bool noWait, int queueSize
                          )
        {

            this.name = name;
            this.when = when;
            this.operation = operation;
            this.forEachRow = forEach;
            this.nowait = noWait;
            this.maxRowsQueued = queueSize;
            this.table = table;
            vectorIndex = SqlToIndex();
           
            rowsQueued = 0;
            pendingQueue = new Queue<TriggerData>();
            runThread = new Thread(new ThreadStart(run));

            int assemblyMarker = triggerClassName.IndexOf("#");
            if (assemblyMarker == -1)
            {
                this.triggerClassName = triggerClassName;
            }
            else
            {//TODOx             
                this.triggerClassName = triggerClassName.Substring(assemblyMarker+1);
                string assemblyFile = triggerClassName.Substring(0, assemblyMarker );
                try
                {
                    assembly = Assembly.LoadFrom(assemblyFile);
                }
                catch (Exception)
                {
                    throw Trace.error(Trace.TRIGGER_FUNCTION_CLASS_NOT_FOUND,
                                  Trace.CREATE_TRIGGER_COMMAND_1);

                }
            }

            if (vectorIndex < 0)
            {
                throw Trace.error(Trace.UNEXPECTED_TOKEN,
                                  Trace.CREATE_TRIGGER_COMMAND_1);
            }

            Type cl;

            try
            {
                if (assembly == null)
                {
                    cl = Type.GetType(this.triggerClassName);
                }
                else
                {
                    cl = assembly.GetType(this.triggerClassName);
                }

            }
            catch (Exception )
            {
                valid = false;
                cl = typeof(DefaultTrigger);
            
            }

            try
            {
                // dynamically instantiate it
                trigger = (Trigger)Activator.CreateInstance(cl);
            }
            catch (Exception)
            {
                valid = false;
                cl = typeof(DefaultTrigger);
                
            }
        }

        /**
         *  Retrieves the SQL character sequence required to (re)create the
         *  trigger, as a StringBuilder
         *
         * @return the SQL character sequence required to (re)create the
         *  trigger
         */
        public StringBuilder getDDL()
        {

            StringBuilder a = new StringBuilder(256);

            a.Append(Token.T_CREATE).Append(' ');
            a.Append(Token.T_TRIGGER).Append(' ');
            a.Append(name.statementName).Append(' ');
            a.Append(when).Append(' ');
            a.Append(operation).Append(' ');
            a.Append(Token.T_ON).Append(' ');
            a.Append(table.getName().statementName).Append(' ');

            if (forEachRow)
            {
                a.Append(Token.T_FOR).Append(' ');
                a.Append(Token.T_EACH).Append(' ');
                a.Append(Token.T_ROW).Append(' ');
            }

            if (nowait)
            {
                a.Append(Token.T_NOWAIT).Append(' ');
            }

            if (maxRowsQueued != getDefaultQueueSize())
            {
                a.Append(Token.T_QUEUE).Append(' ');
                a.Append(maxRowsQueued).Append(' ');
            }

            a.Append(Token.T_CALL).Append(' ');
            a.Append(StringConverter.toQuotedString(triggerClassName, '"',
                    false));

            return a;
        }

        /**
         *  SqlToIndex method declaration <P>
         *
         *  Given the SQL creating the trigger, say what the index to the
         *  HsqlArrayList[] is
         *
         * @return  index to the HsqlArrayList[]
         */
        public int SqlToIndex()
        {

            int indx;

            if (operation.Equals(Token.T_INSERT))
            {
                indx = TriggerConstants.INSERT_AFTER;
            }
            else if (operation.Equals(Token.T_DELETE))
            {
                indx = TriggerConstants.DELETE_AFTER;
            }
            else if (operation.Equals(Token.T_UPDATE))
            {
                indx = TriggerConstants.UPDATE_AFTER;
            }
            else
            {
                return -1;
            }

            if (when.Equals(Token.T_BEFORE))
            {
                indx += TriggerConstants.NUM_TRIGGER_OPS;    // number of operations
            }
            else if (!when.Equals(Token.T_AFTER))
            {
                return -1;
            }

            if (forEachRow)
            {
                indx += 2 * TriggerConstants.NUM_TRIGGER_OPS;
            }

            return indx;
        }

        public static int indexToRight(int idx)
        {

            switch (idx)
            {

                case TriggerConstants.DELETE_AFTER:
                case TriggerConstants.DELETE_AFTER_ROW:
                case TriggerConstants.DELETE_BEFORE:
                case TriggerConstants.DELETE_BEFORE_ROW:
                    return GrantConstants.DELETE;

                case TriggerConstants.INSERT_AFTER:
                case TriggerConstants.INSERT_AFTER_ROW:
                case TriggerConstants.INSERT_BEFORE:
                case TriggerConstants.INSERT_BEFORE_ROW:
                    return GrantConstants.INSERT;

                case TriggerConstants.UPDATE_AFTER:
                case TriggerConstants.UPDATE_AFTER_ROW:
                case TriggerConstants.UPDATE_BEFORE:
                case TriggerConstants.UPDATE_BEFORE_ROW:
                    return GrantConstants.UPDATE;

                default:
                    return 0;
            }
        }

        /**
         *  run method declaration <P>
         *
         *  the trigger JSP is run _in its own thread here. Its job is simply to
         *  wait until it is told by the main thread that it should fire the
         *  trigger.
         */
        public void run()
        {

            while (keepGoing)
            {
                TriggerData triggerData = popPair();

                if (triggerData != null)
                {
                    if (triggerData.username != null)
                    {
                        trigger.fire(this.vectorIndex, name.name,
                                     table.getName().name, triggerData.oldRow,
                                     triggerData.newRow);
                    }
                }
            }
        }

        /**
         * start the thread if this is threaded
         */
        public void start()
        {
            lock (this)
            {
                if (maxRowsQueued != 0)
                {

                    runThread.Start();
                }
            }
        }

        /**
         * signal the thread to stop
         */
        public void terminate()
        {
            lock (this)
            {
                keepGoing = false;               
            }
        }

        /**
         *  pop2 method declaration <P>
         *
         *  The consumer (trigger) thread waits for an event to be queued <P>
         *
         *  <B>Note: </B> This push/pop pairing assumes a single producer thread
         *  and a single consumer thread _only_.
         *
         * @return  Description of the Return Value
         */
        public TriggerData popPair()
        {
            lock (this)
            {
                if (rowsQueued == 0)
                {
                    try
                    {
                        Monitor.Wait(this);    // this releases the lock monitor
                    }
                    catch (ThreadInterruptedException)
                    {

                        /* ignore and resume */
                    }
                }

                rowsQueued--;

                Monitor.Pulse(this);    // notify push's wait

                if (pendingQueue.Count == 0)
                {
                    return null;
                }
                else
                {
                    return (TriggerData)pendingQueue.Dequeue();
                }
            }
        }

        /**
         *  The main thread tells the trigger thread to fire by this call.
         *  If this Trigger is not threaded then the fire method is caled
         *  immediately and executed by the main thread. Otherwise, the row
         *  data objects are added to the queue to be used by the Trigger thread.
         *
         * @param  row1
         * @param  row2
         */
        public void pushPair(Session session, Object[] row1,
                                  Object[] row2)
        {
            lock (this)
            {
                if (maxRowsQueued == 0)
                {
                    trigger.fire(vectorIndex, name.name, table.getName().name, row1,
                                 row2);

                    return;
                }

                if (rowsQueued >= maxRowsQueued)
                {
                    if (nowait)
                    {
                        pendingQueue.Dequeue();    // overwrite last
                    }
                    else
                    {
                        try
                        {
                            Monitor.Wait(this); 
                        }
                        catch (ThreadInterruptedException)
                        {

                            /* ignore and resume */
                        }

                        rowsQueued++;
                    }
                }
                else
                {
                    rowsQueued++;
                }

                pendingQueue.Enqueue(new TriggerData(session, row1, row2));
                Monitor.Pulse(this);    // notify pop's wait
            }
        }

        /**
         *  Method declaration
         *
         * @return
         */
        public bool isBusy()
        {
            return rowsQueued != 0;
        }

        /**
         *  Method declaration
         *
         * @return
         */
        public bool isValid()
        {
            return valid;
        }

        /**
         * Class to store the data used to fire a trigger. The username attribute
         * is not used but it allows developers to change the signature of the
         * fire method of the Trigger class and pass the user name to the Trigger.
         */
        public class TriggerData
        {

            public Object[] oldRow;
            public Object[] newRow;
            public string username;

            public TriggerData(Session session, Object[] oldRow,
                               Object[] newRow)
            {

                this.oldRow = oldRow;
                this.newRow = newRow;
                this.username = session.getUsername();
            }
        }

        public class DefaultTrigger : Trigger
        {

            public void fire(int i, string name, string table, Object[] row1,
                             Object[] row2)
            {
                throw new Exception("Missing Trigger class!");
            }
        }
    }

}
